class ExtractionResult {
  public IsMatch: boolean;
  public Matches: any[];

  constructor(isMatch: boolean) {
    this.IsMatch = isMatch;
    this.Matches = [];
  }
}

enum FormatStringTokenType {
  ConstantText,
  DynamicValue,
}

class FormatStringToken {
  public Text: string;

  public Type: FormatStringTokenType;

  constructor(text: string, type: FormatStringTokenType) {
    this.Text = text;
    this.Type = type;
  }
}

class FormatStringTokenizer {
  Tokenize(format: string, includeBracketsForDynamicValues: boolean = false): FormatStringToken[] {
    const tokens: FormatStringToken[] = [];

    let currentText = '';
    let inDynamicValue = false;

    for (let i = 0; i < format.length; i++) {
      const c = format[i];
      switch (c) {
        case '{':
          if (inDynamicValue) {
            throw new Error(
              'Incorrect syntax at char ' + i + '! format string can not contain nested dynamic value expression!',
            );
          }

          inDynamicValue = true;

          if (currentText.length > 0) {
            tokens.push(new FormatStringToken(currentText, FormatStringTokenType.ConstantText));
            currentText = '';
          }

          break;
        case '}':
          if (!inDynamicValue) {
            throw new Error(
              'Incorrect syntax at char ' + i + '! These is no opening brackets for the closing bracket }.',
            );
          }

          inDynamicValue = false;

          if (currentText.length <= 0) {
            throw new Error('Incorrect syntax at char ' + i + '! Brackets does not containt any chars.');
          }

          let dynamicValue = currentText;
          if (includeBracketsForDynamicValues) {
            dynamicValue = '{' + dynamicValue + '}';
          }

          tokens.push(new FormatStringToken(dynamicValue, FormatStringTokenType.DynamicValue));
          currentText = '';

          break;
        default:
          currentText += c;
          break;
      }
    }

    if (inDynamicValue) {
      throw new Error('There is no closing } char for an opened { char.');
    }

    if (currentText.length > 0) {
      tokens.push(new FormatStringToken(currentText, FormatStringTokenType.ConstantText));
    }

    return tokens;
  }
}

export class FormattedStringValueExtracter {
  Extract(str: string, format: string): ExtractionResult {
    if (str === format) {
      return new ExtractionResult(true);
    }

    const formatTokens = new FormatStringTokenizer().Tokenize(format);
    if (!formatTokens) {
      return new ExtractionResult(str === '');
    }

    const result = new ExtractionResult(true);

    for (let i = 0; i < formatTokens.length; i++) {
      const currentToken = formatTokens[i];
      const previousToken = i > 0 ? formatTokens[i - 1] : null;

      if (currentToken.Type === FormatStringTokenType.ConstantText) {
        if (i === 0) {
          if (str.indexOf(currentToken.Text) !== 0) {
            result.IsMatch = false;
            return result;
          }

          str = str.substr(currentToken.Text.length, str.length - currentToken.Text.length);
        } else {
          const matchIndex = str.indexOf(currentToken.Text);
          if (matchIndex < 0) {
            result.IsMatch = false;
            return result;
          }

          result.Matches.push({ name: previousToken.Text, value: str.substr(0, matchIndex) });
          str = str.substring(0, matchIndex + currentToken.Text.length);
        }
      }
    }

    const lastToken = formatTokens[formatTokens.length - 1];
    if (lastToken.Type === FormatStringTokenType.DynamicValue) {
      result.Matches.push({ name: lastToken.Text, value: str });
    }

    return result;
  }

  IsMatch(str: string, format: string): string[] {
    const result = new FormattedStringValueExtracter().Extract(str, format);
    if (!result.IsMatch) {
      return [];
    }

    const values = [];
    for (let i = 0; i < result.Matches.length; i++) {
      values.push(result.Matches[i].value);
    }

    return values;
  }
}
